﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;
using Noris.Schedule.Support.Parser;

namespace Noris.Tools.DbManager.Data
{
    #region class DevelopmentData : Sada vývojů + skriptů, načtená ze souborů adresáře
    /// <summary>
    /// DevelopmentData : Balíček + Sada vývojů (a jejich skriptů), načtená ze souborů adresáře
    /// </summary>
    internal class DevelopmentData
    {
        #region Konstrukce sady na základě dat v adresáři
		private DevelopmentData(PackageItem package, CabinetInfo cabinet)
        {
			this.Package = package;
			this.Cabinet = cabinet;
            this.Developments = new List<DevelopmentInfo>();
        }
		internal static DevelopmentData CreateForFiles(PackageItem package, CabinetInfo cabinet, List<FileSystemInfo> files)
        {
			DevelopmentData development = new DevelopmentData(package, cabinet);
            FileSystemInfo xmlFile = files.FirstOrDefault(f => String.Equals(f.Name, "patch.txt.xml", StringComparison.InvariantCultureIgnoreCase));
            if (xmlFile != null)
                development.LoadDevelopmentData(xmlFile.FullName);
            development.LoadScriptList(files);
            return development;
        }
        /// <summary>
        /// Načte seznam vývojů ze souboru "patch.txt.xml" (parsuje XML).
        /// </summary>
        /// <param name="patchFileName"></param>
        private void LoadDevelopmentData(string patchFileName)
        {
            Dictionary<string, DevelopmentInfo> dd = new Dictionary<string, DevelopmentInfo>();
            System.Xml.Linq.XDocument document = XDocument.Load(patchFileName);
            foreach (XElement element in document.Root.Elements())
            {   // Root element je "List", jeho elementy mají být "Script":
                if (element.Name == "Script" && element.HasAttributes)
                {
                    DevelopmentInfo development = new DevelopmentInfo(element);
                    if (development.IsValid)
                        dd.Add(development.Development, development);
                }
            }

            this.Developments = new List<DevelopmentInfo>(dd.Values);
            this.Developments.Sort(DevelopmentInfo.CompareByDevelopment);
        }
        /// <summary>
        /// Zpracuje vstupující seznam souborů, vybere z nich SQL a vloží je do odpovídajících vývojů.
        /// </summary>
        /// <param name="files"></param>
        private void LoadScriptList(List<FileSystemInfo> files)
        {
            foreach (FileSystemInfo file in files)
            {
                FileInfo fileInfo = file as FileInfo;
                if (fileInfo != null && String.Equals(fileInfo.Extension, ".sql", StringComparison.InvariantCultureIgnoreCase))
                {   // Soubor s příponou SQL:
                    string name = Path.GetFileNameWithoutExtension(fileInfo.Name);

                    // Najít vývoj, jehož číslo je obsaženo v začátku jména skriptu:
                    DevelopmentInfo development = this.Developments.FirstOrDefault(d => name.StartsWith(d.Development));
                    if (development != null)
                        development.AddScript(fileInfo); 
                }
            }

            // V každém vývoji setřídit skripty:
            foreach (DevelopmentInfo development in this.Developments)
                development.SortScripts();
        }
        #endregion
        #region Property
		/// <summary>
		/// Balíček patche
		/// </summary>
		internal PackageItem Package { get; private set; }
		/// <summary>
		/// CAB umožňující přístup k balíčku
		/// </summary>
		internal CabinetInfo Cabinet { get; private set; }
        /// <summary>
        /// Seznam vývojů
        /// </summary>
        internal List<DevelopmentInfo> Developments { get; private set; }
        /// <summary>
        /// Příznak, že tento balíček obsahuje nějaké platné vývoje se skripty
        /// </summary>
        internal bool ContainAnyScript { get { return (this.Developments.Any(d => d.IsValid && d.ContainScript)); } }
        #endregion
    }
    #endregion
    #region class DevelopmentInfo : Jeden vývoj, jeho popis a podmínky (třídy, legislation), jeho skripty.
    /// <summary>
    /// DevelopmentInfo : Jeden vývoj, jeho popis a podmínky (třídy, legislation), jeho skripty.
    /// </summary>
    internal class DevelopmentInfo
    {
        #region Konstrukce
        /// <summary>
        /// Konstruktor jednoho vývoje z dat elementu
        /// </summary>
        /// <param name="development"></param>
        /// <param name="description"></param>
        /// <param name="classes"></param>
        /// <param name="legislation"></param>
        internal DevelopmentInfo(XElement element)
        {
            if (element.Name == "Script")
            {
                this.Development = element.Attribute("Development").Value.Trim();
                this.Description = element.Attribute("Description").Value;
                this.Classes = ParseClasses(element.Attribute("Classes").Value);
                this.Legislation = element.Attribute("Legislation").Value;
                this.ScriptFiles = new List<FileInfo>();
            }
        }
        /// <summary>
        /// Rozdělí text s čísly (tříd) na pole čísel
        /// </summary>
        /// <param name="classes"></param>
        /// <returns></returns>
        internal List<int> ParseClasses(string classes)
        {
            List<int> result = new List<int>();
            string[] items = classes.Split(',');
            foreach (string item in items)
            {
                int value;
                if (Int32.TryParse(item, out value))
                    result.Add(value);
            }
            return result;
        }
        /// <summary>
        /// Vizualizace
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return this.Development + ": " + this.Description + "  (" + this.ScriptFiles.Count.ToString() + " scripts)";
        }
        #endregion
        #region Property
        /// <summary>
        /// Vývoj (číslo vývoje, typicky "0041012")
        /// </summary>
        internal string Development { get; private set; }
        /// <summary>
        /// Popis vývoje = název subjektu
        /// </summary>
        internal string Description { get; private set; }
        /// <summary>
        /// Třídy "Skriptovat podle..."
        /// </summary>
        internal List<int> Classes { get; private set; }
        /// <summary>
        /// Legislativa
        /// </summary>
        internal string Legislation { get; private set; }
        /// <summary>
        /// Seznam souborů skriptů, je setříděný
        /// </summary>
        internal List<FileInfo> ScriptFiles { get; private set; }
        /// <summary>
        /// Příznak platnosti = je uveden vývoj a třídy "Skriptovat podle..."
        /// </summary>
        internal bool IsValid { get { return (this.Development.Trim().Length > 0 && this.Classes.Count > 0); } }
        /// <summary>
        /// Příznak, že tento vývoj obsahuje nějaké soubory se skriptem
        /// </summary>
        internal bool ContainScript { get { return (this.ScriptFiles.Count > 0); } }
        #endregion
        #region Podpora pro tvorbu, komparátory pro třídění
        /// <summary>
        /// Přidá další soubor skriptu
        /// </summary>
        /// <param name="fileInfo"></param>
        internal void AddScript(FileInfo fileInfo)
        {
            this.ScriptFiles.Add(fileInfo);
        }
        /// <summary>
        /// Setřídí soubory skriptů
        /// </summary>
        internal void SortScripts()
        {
            if (this.ScriptFiles.Count > 1)
                this.ScriptFiles.Sort((a, b) => String.Compare(a.Name, b.Name));
        }
        /// <summary>
        /// Komparátor podle čísla vývoje
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        internal static int CompareByDevelopment(DevelopmentInfo a, DevelopmentInfo b)
        {
            if (a == null && b == null) return 0;
            if (a == null) return -1;
            if (b == null) return 1;
            return String.Compare(a.Development, b.Development, StringComparison.InvariantCultureIgnoreCase);
        }
        #endregion
		#region Práce s textem scriptu
        /// <summary>
        /// Vrátí text jednotlivých dávek (SQL texty oddělené slovem GO) z daného skriptu
        /// </summary>
        /// <param name="fileInfo"></param>
        /// <returns></returns>
		internal static List<string> GetSqlBatchFromScript(FileInfo fileInfo)
		{
			string content = System.IO.File.ReadAllText(fileInfo.FullName, Encoding.Default);
			ParserSetting msSqlSetting = ParserDefaultSetting.MsSql;                   // Předpis parsování MS-SQL textu
			List<ParserSegment> commands = Parser.ParseString(content, msSqlSetting);  // Parser vytvoří jednotlivé segmenty, oddělené znakem ";" (oddělovač příkazů).

			List<ParserSegment> batches = SplitToBatch(commands, true, true);
			List<string> result = batches.ConvertAll(s => s.InnerText);
			return result;
		}
        /// <summary>
        /// Rozdělí sadu SQL příkazů a jejich segmentů do sady dávek, oddělených slovem GO.
        /// </summary>
        /// <param name="commands"></param>
        /// <returns></returns>
        internal static List<ParserSegment> SplitToBatch(List<ParserSegment> commands)
        {
            return SplitToBatch(commands, false, false);
        }
        /// <summary>
        /// Rozdělí sadu SQL příkazů a jejich segmentů do sady dávek, oddělených slovem GO.
        /// </summary>
        /// <param name="commands"></param>
        /// <returns></returns>
		internal static List<ParserSegment> SplitToBatch(List<ParserSegment> commands, bool removeEmpty, bool removeOnlyComments)
		{
			List<ParserSegment> batches = new List<ParserSegment>();
			ParserSegment currentBatch = null;
			SplitState state = SplitState.IntoNewBatch;
			List<ParserSegmentValue> buffer = null;
			char[] eols = new char[] { '\r', '\n' };
			foreach (ParserSegment command in commands)
			{	// Jednotlivé příkazy oddělené ";"
				foreach (ParserSegmentValue value in command.Values)
				{	// Jednotlivé součásti SQL příkazu (slova, celé závorky, celé stringy, oddělovače, blank):
					bool isEol = (value.ValueType == ParserSegmentValueType.Blank && value.Content.IndexOfAny(eols) >= 0);
					if (isEol)
					{	// Konec řádku nás zajímá:
						switch (state)
						{
							case SplitState.IntoNewBatch:
								// Aktuální text jde do nové dávky: ta nebude začínat EOL. Nic nedělám, ignoruji EOL.
								// Stav necháváme beze změn.
								break;
							case SplitState.IntoCurrentBatch:
								// Je to EOL a má jít do aktuální dávky: dám ho tam, ale příští Values půjdou do Bufferu:
								currentBatch.AddValue(value);
								buffer = new List<ParserSegmentValue>();
								state = SplitState.IntoBuffer;
								break;
							case SplitState.IntoBuffer:
								// Konec řádku, a řádek šel do bufferu: to nás obzvláště zajímá, tady může být řádek obsahující pouze slovo GO:
                                buffer.Add(value);                        // Konec tohoto řádku dáme do bufferu
                                if (BufferContainOnlyText(buffer, "GO"))
                                {   // V bufferu je pouze jedno slovo GO, a nic jiného = jde o oddělovač batch:
                                    // Další text půjde do nové dávky (pokud bude):
                                    state = SplitState.IntoNewBatch;
                                    buffer.Clear();
                                }
                                else
                                {   // V bufferu je nastřádaný jiný text než slovo GO, musíme jej zcela beze změn (a to včetně posledního EOL) vložit do aktuální dávky, 
                                    //  ale protože jsme našli EOL, tak další data zase půjdou do bufferu - do čistého:
                                    if (currentBatch == null)
                                        currentBatch = CreateNewBatch(command, batches);
                                    currentBatch.AddValues(buffer);
                                    buffer.Clear();
                                }
								break;
						}
					}
                    else if (value.ValueType == ParserSegmentValueType.Text && String.Equals(value.Content, "GO", StringComparison.InvariantCultureIgnoreCase))
                    {   // Slovo GO, typicky na počátku nového segmentu:
                        if (state == SplitState.IntoCurrentBatch)
                            // Pokud jsem dosud dával položky do aktuální dávky, pak tímto dávku ukončím.
                            // V seznamu dávek tato dávka je, a další položky už půjdou do nové dávky.
                            currentBatch = null;

                        // Slovo GO dám do bufferu, a vše další rovněž:
                        if (buffer == null)
                            buffer = new List<ParserSegmentValue>();
                        buffer.Add(value);
						state = SplitState.IntoBuffer;
                    }
                    else
					{	// Není to EOL, a není to ani GO: dáme to, kam se patří:
                        switch (state)
                        {
                            case SplitState.IntoNewBatch:
                                currentBatch = CreateNewBatch(command, batches);
                                currentBatch.AddValue(value);
                                state = SplitState.IntoCurrentBatch;
                                break;
                            case SplitState.IntoCurrentBatch:
                                if (currentBatch == null)
                                    currentBatch = CreateNewBatch(command, batches);
                                currentBatch.AddValue(value);
                                break;
                            case SplitState.IntoBuffer:
                                buffer.Add(value);
                                break;
                        }
					}
				}
			}

            // Co s daty v bufferu, pokud tam jsou?
            if (buffer != null && buffer.Count > 0 && !BufferContainOnlyText(buffer, "GO") && commands.Count > 0)
            {   // Přidáme buffer do poslední dávky:
                if (currentBatch == null)
                    currentBatch = CreateNewBatch(commands[commands.Count - 1], batches);
                currentBatch.AddValues(buffer);
                buffer.Clear();
            }

            // Vyřadit některé položky:
            if (removeEmpty || removeOnlyComments)
            {
                List<ParserSegment> batchesValid = new List<ParserSegment>();
                foreach (ParserSegment batch in batches)
                {
                    bool ignoreEmpty = removeEmpty && SegmentIsEmpty(batch);
                    bool ignoreCommn = removeOnlyComments && SegmentIsEmptyOrComment(batch);
                    if (!ignoreEmpty && !ignoreCommn)
                        batchesValid.Add(batch);
                }
                batches = batchesValid;
            }

			return batches;
		}
        /// <summary>
        /// Vrací true, pokud buffer obsahuje pouze daný text, a to pouze jedenkrát.
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="find"></param>
        /// <returns></returns>
        private static bool BufferContainOnlyText(List<ParserSegmentValue> buffer, string find)
        {
            int occurs = 0;
            foreach (ParserSegmentValue value in buffer)
            {
                switch (value.ValueType)
                {
                    case ParserSegmentValueType.Blank:
                        // Prázdno nám nevadí (mezera, TAB, CR, LF)
                        break;
                    case ParserSegmentValueType.Delimiter:
                        // Oddělovač (+-*/ atd) - ty zde nemohou být (oddělovač není znak ";" protože to není Delimiter, ale ukončovací znak příkazu)
                        return false;
                    case ParserSegmentValueType.Text:
                        // Text - To nás zajímá:
                        if (String.Equals(value.Content, find, StringComparison.InvariantCultureIgnoreCase))
                        {   // Je to náš hledaný text?
                            occurs++;
                            if (occurs > 1)
                                // Pokud je tam víc než 1x, nesouhlasí to:
                                return false;
                        }
                        else
                        {   // Je to něco jiného:
                            return false;
                        }
                        break;
                    case ParserSegmentValueType.InnerSegment:
                        // Obsahuje vnořený segment, a to nesmí (typicky komentář, string, závorka):
                        return false;
                }
            }
            return (occurs == 1);
        }
        /// <summary>
        /// Vrací true, pokud daný segment je Empty.
        /// Pokud obsahuje jakoukoli Value typu Delimiter, InnerSegment nebo Text, pak není Empty.
        /// </summary>
        /// <param name="segment"></param>
        /// <returns></returns>
        private static bool SegmentIsEmpty(ParserSegment segment)
        {
            foreach (ParserSegmentValue value in segment.Values)
            {
                switch (value.ValueType)
                {
                    case ParserSegmentValueType.Delimiter:
                    case ParserSegmentValueType.InnerSegment:
                    case ParserSegmentValueType.Text:
                        return false;
                }
            }
            return true;
        }
        /// <summary>
        /// Vrací true, pokud daný segment je Empty anebo obsahuje pouze komentáře.
        /// Pokud obsahuje jakoukoli Value typu Delimiter, InnerSegment nebo Text, pak není Empty.
        /// </summary>
        /// <param name="segment"></param>
        /// <returns></returns>
        private static bool SegmentIsEmptyOrComment(ParserSegment segment)
        {
            foreach (ParserSegmentValue value in segment.Values)
            {
                switch (value.ValueType)
                {
                    case ParserSegmentValueType.InnerSegment:
                        string segmentName = value.InnerSegment.SegmentName;
                        bool isComment = (segmentName == ParserDefaultSetting.SQL_COMMENTLINE || segmentName == ParserDefaultSetting.SQL_COMMENTBLOCK);
                        if (!isComment)
                            return false;
                        break;
                    case ParserSegmentValueType.Delimiter:
                    case ParserSegmentValueType.Text:
                        return false;
                }
            }
            return true;
        }

        /// <summary>
        /// Vytvoří a vrátí novou dávku příkazů.
        /// </summary>
        /// <param name="command">Vzorový segment.</param>
        /// <param name="batches"></param>
        /// <returns></returns>
        private static ParserSegment CreateNewBatch(ParserSegment command, List<ParserSegment> batches)
        {
            ParserSegment batch = new ParserSegment(null, command);
            batches.Add(batch);
            return batch;
        }
        private enum SplitState
		{
			None = 0,
			IntoNewBatch,
			IntoCurrentBatch,
			IntoBuffer
		}
		#endregion
    }
    #endregion
}
